// ====================================================================================
// DATA STORAGE OBJECTS
// ====================================================================================

//Stores inds for a job
function JobIndsData() {
    this.inds = [];
    
    // Method to add an ind (will be overwritten if it exists already)
	// The id of the ind must be equal to the position in this.inds
    this.update = update;
    function update(new_inds) {
        LOG("    JobEvalsData.update");
        for (var i=0; i<new_inds.length; i++) {
            var new_ind = new_inds[i];
            var id = new_ind.__id__;
            if (id < this.inds.length) {
                this.inds[id]=new_ind;
            } else {
                for (var i=this.inds.length; i<id; i++) {
                    this.inds.push({}); //this will be replaced later
                }
                this.inds.push(new_ind);
            }
        }
    }
    
    // Method to get an array of all inds
    this.get_inds_array = get_inds_array;
    function get_inds_array() {
        LOG("    JobEvalsData.get_inds_array");
        return this.inds;
    }
    
    // Method to get the number of inds
    this.num_inds = num_inds;
    function num_inds() {
        LOG("    JobEvalsData.num_inds");
        return this.inds.length;
    }
    
    // Method to get a str representation
    this.str = str;
    function str() {
        LOG("    JobEvalsData.str");
        return "    Num inds = " + this.inds.length;
    }
}

// Stores evals for a job
function JobEvalsData() {
    this.evals = {};
    this.eval_names = []; //could use d3.set
    
    // Method to initialise the evaluations
    this._initialise = _initialise;
    function _initialise(first_inds) {
        LOG("    JobEvalsData._initialise");
        for (var i = 0; i < first_inds.length; i++) {
            for (property in first_inds[i]) {
                if (property.charAt(0) != "_") {
                    if (! this.evals.hasOwnProperty(property)) {
                        this.evals[property] = {min:null, max:null};
                        this.eval_names.push(property);
                    }
                }
            }
        }
    }
    
    // Method to update evals from new inds 
    this.update = update;
    function update(new_inds) {
        LOG("    JobEvalsData.update");
        if (this.eval_names.length == 0) {
            this._initialise(new_inds); //this is only done the first time
			update_dropdown_options();
        }
        for (eval_name in this.evals) { //could use d3.extent
            var values = new_inds.map(function(new_ind){return new_ind[eval_name];});
            values = values.concat([this.evals[eval_name].min, this.evals[eval_name].max]);
            this.evals[eval_name] = {min:d3.min(values), max:d3.max(values)};
        }
    }
    
    // Method to get domain of eval
    this.get_domain = get_domain;
    function get_domain(eval_name) {
        LOG("    JobEvalsData.get_domain");
        var domain;
        if (! this.evals.hasOwnProperty(eval_name)) {
            domain = [0,1];
        } else {
            domain = [this.evals[eval_name].min, this.evals[eval_name].max];
        }
        return domain;
    }
    
    // Method to get an array of all evals
    this.get_evals_array = get_evals_array;
    function get_evals_array() {
        LOG("    JobEvalsData.get_evals_array");
        return this.evals_array;
    }
    
    // Method to get number of evals
    this.num_evals = num_evals;
    function num_evals() {
        LOG("    JobEvalsData.num_evals");
        return this.eval_names.length;
    }
    
    // Method to get a str
    this.str = str;
    function str() {
        LOG("    JobEvalsData.str");
        var result = "    Evals = " + this.eval_names.join();
        for (eval_name in this.evals) {
            result = result + "\n    " + eval_name + " : " + this.evals[eval_name].min + ", " + this.evals[eval_name].max;
        }
        return result;
    }
}

// ====================================================================================
// FUNCTIONS
// ====================================================================================

// Update the dropdown select menus for the axes
// Call when the JobEvalsData object is first initialised
function update_dropdown_options() {
    LOG("update_dropdown_options");
	
    //get the option lists
    var x_axis_name_dropdown = document.getElementById('x_axis_name_dropdown');
    var y_axis_name_dropdown = document.getElementById('y_axis_name_dropdown');

    //create options for the drop down
    for (var i=0;i<evals_data[job_id].num_evals();i++) {
        x_axis_name_dropdown.options[i] = new Option(evals_data[job_id].eval_names[i], i);
        y_axis_name_dropdown.options[i] = new Option(evals_data[job_id].eval_names[i], i);
    }
    //make default selections
    x_axis_name_dropdown.selectedIndex = 0;
    y_axis_name_dropdown.selectedIndex = 1; 
    x_axis_name = x_axis_name_dropdown.options[0].text;
    y_axis_name = y_axis_name_dropdown.options[1].text;
    
    LOG("    x_axis_name = " + x_axis_name + ", y_axis_name = " + y_axis_name);
}

// Update the scale functions
// Call either on a window resize or on_inds_updated, on_job_changed
function update_scales() {
    LOG("update_scales");

	// Ranges functiions
	function x_range() {return [svg_margin_left, svg_width() - svg_margin_right];}
	function y_range() {return [svg_height() - svg_margin_bottom, svg_margin_top];}
	
    // Get the domains
    var x_domain = evals_data[job_id].get_domain(x_axis_name);
    var y_domain = evals_data[job_id].get_domain(y_axis_name);
	
    // The x and y scale functions
    xScale = d3.scale.linear().domain(x_domain).range(x_range());
    yScale = d3.scale.linear().domain(y_domain).range(y_range());
	
	// The x and y axis functions
    xAxis.scale(xScale);
    yAxis.scale(yScale);
	
	// Set the zoom settings for the axes
	zoom.x(xScale).y(yScale);
	//zoom.scale(zoom_scale);
	//zoom.translate(zoom_translate);
	
    LOG("    x domain = " + x_domain + "  >>  x range = " + x_range());
    LOG("    y domain = " + y_domain + "  >>  y range = " + y_range());
}

// Resize the svg container and box
// Call only on a window resize event
function resize_background() {
    LOG("resize_background");
    
    // Resize the svg
    svg_container.
		attr("width", svg_width()).
		attr("height", svg_height());
		//.call(zoom);

    // Resize the box
    rect.
      attr("width", svg_width()).
      attr("height", svg_height());
}

// Delete all the points (circles and labels)
// Call on_job_changed
function delete_points() {
    LOG("delete_points");
    LOG(inds_data[job_id].str());
    
    // Get the data
    data_array = inds_data[job_id].get_inds_array();
    var d3_circles = svg_group.selectAll("circle.point").data(data_array); //, function(ind) {return ind.__id__;});
    var d3_labels = svg_group.selectAll("text.point").data(data_array) //, function(ind) {return ind.__id__;});
    
    // Job changed, remove everything
    d3_circles.remove();
    d3_labels.remove();

}

// Redraw the graph (points, point labels, axes, axes labels)
// Call any time the graph changes - window resize, on_inds_updated, on_job_changed
function draw_graph(transition_duration) {
    LOG("draw_graph");
    LOG(inds_data[job_id].str());
    
	// Function to hide points with missing evaluations
	// So points with missing data will be hidden. Later when the data is updated, they will be 
	// shown (via the 'UPDATE' circles).
	function filter_data(ind) {
		if (! ind.hasOwnProperty(x_axis_name) || ! ind.hasOwnProperty(y_axis_name)){
			return "point hide"; 
		} else {
			return "point show";
		}
	}
	
    // Get the data
    data_array = inds_data[job_id].get_inds_array();
    var d3_circles = svg_group.selectAll("circle.point").data(data_array); //, function(ind) {return ind.__id__;});
    var d3_labels = svg_group.selectAll("text.point").data(data_array); //, function(ind) {return ind.__id__;});

	LOG(zoom_translate);
	LOG(zoom_scale);
	
    // UPDATE circles selection
    d3_circles.transition().duration(transition_duration).
        attr("class", filter_data).
        attr("cx",function(ind) {return xScale(ind[x_axis_name]);}).
        attr("cy",function(ind) {return yScale(ind[y_axis_name]);}).
        attr("r", function(ind) {if (ind._inact_time == null) { return 3;} else {return 2;}}).
        attr("fill", function(ind) {if (ind._inact_time == null) { return "blue";} else {return "black";}}).
		attr("transform", "translate(" + zoom_translate + ")	 scale(" + zoom_scale + ")"); 
    // ENTER circles selection
    d3_circles.enter().append("circle").
        attr("class", filter_data).
        attr("cx",function(ind) {return xScale(ind[x_axis_name]);}).
        attr("cy",function(ind) {return yScale(ind[y_axis_name]);}).
        attr("r", function(ind) {if (ind._inact_time == null) { return 3;} else {return 2;}}).
        attr("fill", function(ind) {if (ind._inact_time == null) { return "blue";} else {return "black";}}).
		attr("transform", "translate(" + zoom_translate + ") scale(" + zoom_scale + ")"); 
    
    
    if (display_labels) {
        // UPDATE labels selection
        d3_labels.transition().
            attr("class", filter_data).                    
            attr("x",function(ind) {return xScale(ind[x_axis_name]) + label_offset;}).
            attr("y",function(ind) {return yScale(ind[y_axis_name]) - label_offset;}).
            attr("fill", "black").
			attr("transform", "translate(" + zoom_translate + ") scale(" + zoom_scale + ")"); 
        // ENTER labels selection
        d3_labels.enter().append("text").
            attr("class", filter_data).
            attr("x",function(ind) {return xScale(ind[x_axis_name]) + label_offset;}).
            attr("y",function(ind) {return yScale(ind[y_axis_name]) - label_offset;}).
            attr("fill", "black").
            text(function(ind) {return ind.__id__;}).
			attr("transform", "translate(" + zoom_translate + ") scale(" + zoom_scale + ")"); 
    } else {
        d3_labels.remove();
        d3_labels.exit().remove();
    }
    
    // Update the axes
	//zoom.scale(zoom_scale);
	//zoom.translate(zoom_translate);
    x_axis_g.
		attr("transform", "translate( 0 ," + (svg_height() - svg_margin_bottom) +")").
		call(xAxis);
    y_axis_g.
		attr("transform", "translate(" + svg_margin_left + ", 0 )").
		call(yAxis);
	
	// Update the axes labels
    x_axis_l.
		attr("x", svg_width()/2 ).
		attr("y", svg_height() -10 ).
		text(x_axis_name);
    y_axis_l.
		attr("x", 0 - (svg_height()/2) ).
		attr("y", 10).
		text(y_axis_name);
}

// ====================================================================================
// HELPER FUNCTIONS
// ====================================================================================

// Debugging
function LOG(msg) {
    if (debug) {console.log(msg);}
}

// Width and height functions
function svg_width() {return $(window).width() - 20;}
function svg_height() {return $(window).height() - 90;}

// ====================================================================================
// DEXEN EVENTS
// ====================================================================================

// DEXEN EVENT: Get the updated inds
function on_inds_updated(job_info, new_inds) {
LOG("==================================================");
    LOG("on_inds_updated...");
    
    // Check that we have some inds
    if (!  new_inds) {LOG("No new_inds found..."); return;}
    
    // Update the inds (add new inds to array)
    inds_data[job_id].update(new_inds);
    
    // Update the evals (min amd max)
    evals_data[job_id].update(new_inds);
    
    // Update
    update_scales();
    draw_graph(1000);
    
    LOG("All jobs:");
    for (job_id in inds_data) {
        LOG("    " + inds_data[job_id].str())
    }
    
}

//  DEXEN EVENT: Job changed
function cur_job_changed(job_info) {
    LOG("==================================================");
    LOG("cur_job_changed...");
    
    // Delet all existing points (circles and labels)
    delete_points();
    
    //Check we have a job
    if (job_info == null) {
        job_id = -1;
        $("#job_id").text("(No job selected.)");
    } else {
        job_id = job_info.job_id;
        $("#job_id").text("Job ID: " + job_id);
    }
    
    // Create new objects for new job
    if (! job_id in inds_data || ! inds_data[job_id]) {
        inds_data[job_id] = new JobIndsData();
    }
    if (! job_id in evals_data || ! evals_data[job_id]) {
        evals_data[job_id] = new JobEvalsData();
    } 
    
    // Update
    update_scales();
    resize_background();
    draw_graph(0);
	
    LOG("    New Job ID is " + job_id);
}

// ====================================================================================
// UI EVENTS
// ====================================================================================

// Testing the zoom event listener
function zoomed(e) {
    LOG("zoomed");
	
	// Get the scale and translate
	zoom_scale = d3.event.scale;
	zoom_translate = d3.event.translate;
	
	// Redraw the graph
	draw_graph(0);
	
	// Redraw the axes
	zoom.scale(zoom_scale);
	zoom.translate(zoom_translate);
	x_axis_g.call(xAxis);
    y_axis_g.call(yAxis);
}


// JQUERY EVENT: Ready
$(document).ready(function() {
    LOG("document ready");

	// Create objects to store dummy data
    inds_data[-1] = new JobIndsData();
    evals_data[-1] = new JobEvalsData();
    
	// Create the two functions that generate the axes
    xAxis = d3.svg.axis().orient("bottom");
    yAxis = d3.svg.axis().orient("left");
	
    // Create SVG elements (add all attrs that do not change)
    svg_container = d3.select("body").append("svg").append("g").
		style("background", "#eeeeee");
    rect = svg_container.append("svg:rect").
		attr("fill", "white").
		attr("fill-opacity", 1).
		attr("stroke", "black").
		attr("stroke-width", 1);
    x_axis_g = svg_container.append("g").
		attr("class", "x axis").
		call(xAxis);
    y_axis_g = svg_container.append("g").
		attr("class", "y axis").
		call(yAxis);
    x_axis_l = svg_container.append("text").
		attr("class", "axis_label").
		attr("text-anchor", "middle");
    y_axis_l = svg_container.append("text").
		attr("class", "axis_label").
		attr("transform", "rotate(-90)").
		attr("text-anchor", "middle").
		attr("dy", "1em");
		
    // Update csales and background (append to svg_conntainer)
    update_scales();
    resize_background();

	// Zooming
	zoom.
		scaleExtent([-10, 10]).
		on("zoom", zoomed);
	svg_group = svg_container.call(zoom).append("g");
	
	// Update graph (append to svg_group)
    draw_graph(0);
	
    // Get already loaded inds
	if (typeof(dexen) !== 'undefined') {
		LOG("    dexen.get_all_inds()");
		
		var cur_job = $.parseJSON(dexen.get_cur_job());
		cur_job_changed(cur_job);

		var inds = $.parseJSON(dexen.get_all_inds());
		for (var i = 0; i < inds.length; i++) {
			on_inds_updated(inds[i].job_info, inds[i].inds);
		}
	}
	
	// WINDOW EVENT: resize
	$(window).resize(function(){
		LOG("window resize");

		// Resize the svg element
		d3.select("svg").attr("width", svg_width()).attr("height", svg_height());
		
		//Refresh
		update_scales();
		resize_background();
		draw_graph(0);
	});
	
	// CHECKBOX EVENT: click
	$("#display_labels_checkbox").click(function(){
		LOG("display_labels_checkbox click, " + this.checked);
		
		if (this.checked) {
			display_labels=true;
		} else {
			display_labels=false;
		}
		draw_graph(0);
	});
	
	// BUTTON EVENT: click
	$("#zoom_all").click(function(){
		LOG("zoom_all click");
		
		// Get the scale and translate
		zoom_scale = 1;
		zoom_translate = [0,0];
		
		// Redraw the graph
		draw_graph(0);
		
		// Redraw the axes
		zoom.scale(zoom_scale);
		zoom.translate(zoom_translate);
		x_axis_g.call(xAxis);
		y_axis_g.call(yAxis);
	});
	
	// SELECT EVENT: change (Dropdown selection event)
	$("#x_axis_name_dropdown").change(function() {
		LOG("x_axis_name_dropdown change");

		// Set the x axis
		x_axis_name = $('#x_axis_name_dropdown option:selected').text();
		//Do a refresh
		update_scales();
		draw_graph(1000);
	});
	
	// SELECT EVENT: change (Dropdown selection event)
	$("#y_axis_name_dropdown").change(function() {
		LOG("y_axis_name_dropdown change");

		// Set the y axis
		y_axis_name = $('#y_axis_name_dropdown option:selected').text();
		//Do a refresh
		update_scales();
		draw_graph(1000);
	});
	
	// JQUERY EVENT: mousemove
	/*
	$(document).mousedown(function(e){
	  //$('#mousepos').html(e.pageX +', '+ e.pageY +" : "+ xScale.invert(e.pageX) +', '+ yScale.invert(e.pageY));
	  LOG("mousedown");

	});
	*/
	
});



// ====================================================================================
// SCRIPT
// ====================================================================================

// Set debug state to true to print messages
var debug = 1;

//The axes
var x_axis_name = "evalA";
var y_axis_name = "evalB";
var xScale, yScale;
var xAxis, yAxis;
var x_axis_g, y_axis_g;
var x_axis_l, y_axis_l;

// All the inds in all jobs
// The data is stored in objects
var job_id = -1;
var inds_data = {};
var evals_data = {};

//Other data
var display_labels = false;

// SVG settings
var svg_container, svg_group;
var svg_margin_bottom = 50;
var svg_margin_left = 70;
var svg_margin_right = 40;
var svg_margin_top = 40;
var label_offset=3;

// Zooming behaviour
var zoom = d3.behavior.zoom();
var zoom_translate = [0,0];
var zoom_scale = 1;




